﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.DirectoryServices.Protocols;
using System.Linq;
using System.Threading.Tasks;
using LinqToLdap.Collections;
using LinqToLdap.Helpers;
using LinqToLdap.Logging;
using LinqToLdap.Mapping;

namespace LinqToLdap.Tests.PopulateDirectory
{
    class Program
    {
        private const string ServerName = "localhost";
        private const string ServerRoot = "CN=Employees,DC=Northwind,DC=local";
        private const string UserDirectoryContainer = "CN=Users," + ServerRoot;
        private const string InheritanceDirectoryContainer = "CN=InheritanceTest," + ServerRoot;
        private const string TestUserDirectoryContainer = "CN=Users2," + ServerRoot;
        private const string RolesDirectoryContainer = "CN=Roles," + ServerRoot;

        static void Main()
        {
            var configuration = new LdapConfiguration()
                .UseStaticStorage()
                .LogTo(new SimpleTextLogger(Console.Out) {TraceEnabled = false})
                .AddMapping(new AttributeClassMap<LdsUser>(), UserDirectoryContainer);

            configuration.ConfigurePooledFactory(ServerName)
                .AuthenticateBy(AuthType.Negotiate)
                //.AuthenticateAs(new NetworkCredential("CN=AlphaUser,CN=Users,CN=Employees,DC=Northwind,DC=local", "test"))
                .MaxPoolSizeIs(100);

            
            AddContainerIfNecessary(typeof(LdsUser));
            PopulateDirectory();
            PopulateDirectoryForTests();
            PropulateDirectoryForInheritance();

            //allows setting passwords without SSL. Only tested for AD LDS
            //EnablePasswordChangesOn389();

            Console.ReadLine();
        }

        #region PopulateDirectory Code

        private static void PopulateDirectory()
        {
            using (var context = new DirectoryContext())
            {
                int entryCount = context.Query<LdsUser>().Count();
                if (entryCount > 1)
                {
                    Console.WriteLine("Directory already populated with {0} entries", entryCount);
                    return;
                }
            }

            var employees = new LdsUser[50000];
            Parallel.For(0, 50000, i =>
            {
                var firstName = GetRandomName() + i;
                var lastName = GetRandomName() + i;
                var distinguishedName = "CN=" + firstName + " " + lastName + "," + UserDirectoryContainer;
                var employee = new LdsUser
                {
                    DistinguishedName = distinguishedName,
                    Employees = new Collection<LdsUser>(),
                    FirstName = firstName,
                    LastName = lastName,
                    EmployeeId = 50 + i,
                    Comment = "This entry was generated by random data.",
                    PhoneNumber = "(123) 555-9857",
                    Title = GetRandomTitle(),
                    Street = "1234 Cool St.",
                    City = GetRandomCity(),
                    Country = GetRandomCountry(),
                    PostalCode = "12345"
                };

                employees[i] = employee;
            });

            var random = new Random(10000);

            int count = 0;
            foreach (var employee in employees)
            {
                if (count % 7 == 0)
                {
                    var index = random.Next(0, 50000);
                    var e = employees[index];
                    if (e != employee && e.Manager == null)
                    {
                        e.Manager = employee.DistinguishedName;
                        employee.Employees.Add(e);
                    }
                }
                count++;
            }

            var managers = employees.Where(e => e.Manager == null).ToList();

            foreach (var employee in managers.AsParallel())
            {
                using (var context = new DirectoryContext())
                {
                    context.Add(employee);

                    if (employee.Employees != null && employee.Employees.Count > 0)
                    {
                        foreach (var subordinate in employee.Employees)
                        {
                            if (subordinate.Manager != employee.DistinguishedName)
                            {
                                throw new Exception("What?");
                            }
                            context.Add(subordinate);
                        }
                    }
                }
                Console.WriteLine("{0} added", employee.DistinguishedName);

                if (employee.Employees == null) continue;

                foreach (var e in employee.Employees)
                {
                    Console.WriteLine("{0} added", e.DistinguishedName);
                }
            }
        }

        private static readonly string[] Names = new[]
                                     {
                                         "John", "Smith", "Stuff", "Jack", "Holly", "Michael", "Ryan", "Pam", "Rebekah",
                                         "Michelle", "Fred", "Terrinski", "Jamie", "Hector", "Eva", "Khrystopher"
                                     };

        private static readonly Random NameRandom = new Random(1000);
        private static string GetRandomName()
        {
            var index = NameRandom.Next(0, 16);
            return Names[index];
        }

        private static readonly string[] Titles = new[]
                                     {
                                         "Kinght", "Doer of Stuff", "Manager", "Programmer", "Overlord", "Ninja", "Warlord", "Title", 
                                         "Vice President", "Supervisor", "Chef", "Producer", "Director", "Owner", "CIO"
                                     };

        private static readonly Random TitleRandom = new Random(1000);
        private static string GetRandomTitle()
        {
            var index = TitleRandom.Next(0, 15);
            return Titles[index];
        }

        private static readonly string[] Cities = new[]
                                     {
                                         "Oklahoma City", "London", "Los Angeles", "Modena", "Sacramento", "Boston", "Dallas", "Dortmund", 
                                         "Mexico City", "Kortrijk", "Rome", "Stavanger", "Milnerton", "Calgary", "Bilbao"
                                     };

        private static readonly Random CityRandom = new Random(1000);
        private static string GetRandomCity()
        {
            var index = CityRandom.Next(0, 15);
            return Cities[index];
        }

        private static readonly string[] Countries = new[]
                                     {
                                         "US", "UK", "IT", "DE", "MX", "BE", "CA", "NO", "ZA", "ES"
                                     };

        private static readonly Random CountryRandom = new Random(1000);
        private static string GetRandomCountry()
        {
            var index = CountryRandom.Next(0, 10);
            return Countries[index];
        }

        #endregion PopulateDirectory Code

        #region PopluateDirectoryForTests Code

        private static void PropulateDirectoryForInheritance()
        {
            AddContainerIfNecessary("CN", "InheritanceTest");

            using (var context = new DirectoryContext())
            {
                int entryCount = context.Query(InheritanceDirectoryContainer).Count();
                if (entryCount > 1)
                {
                    Console.WriteLine("Inheritince Directory already populated with {0} entries", entryCount);
                    return;
                }
            }

            var users = new DirectoryAttributes[10000];
            Parallel.For(0, 10000, i =>
            {
                var firstName = GetRandomName() + i;
                var lastName = GetRandomName() + i;
                var distinguishedName = "CN=" + firstName + " " + lastName + "," + InheritanceDirectoryContainer;

                var user = new DirectoryAttributes(distinguishedName);

                var objectClass = i%5 == 0 ? "user" : (i%3 == 0 ? "organizationalPerson" : "person");

                user.Set("objectClass", objectClass);

                if (objectClass == "user")
                {
                    user.Set("givenname", firstName)
                        .Set("sn", lastName)
                        .Set("employeeid", 50 + i)
                        .Set("comment", "This entry was generated by random data.")
                        .Set("telephonenumber", "(123) 555-9857")
                        .Set("title", GetRandomTitle())
                        .Set("street", "1234 Cool St.")
                        .Set("l", GetRandomCity())
                        .Set("c", GetRandomCountry())
                        .Set("PostalCode", "12345");
                }
                else if (objectClass == "person")
                {
                    user.Set("sn", lastName);
                }
                else
                {
                    user.Set("givenname", firstName)
                        .Set("sn", lastName)
                        .Set("employeeid", 50 + i)
                        .Set("comment", "This entry was generated by random data.")
                        .Set("telephonenumber", "(123) 555-9857")
                        .Set("title", GetRandomTitle())
                        .Set("street", "1234 Cool St.")
                        .Set("l", GetRandomCity())
                        .Set("c", GetRandomCountry())
                        .Set("postalCode", "12345");
                }
                

                users[i] = user;
            });
            
            foreach (var user in users.AsParallel())
            {
                using (var context = new DirectoryContext())
                {
                    context.Add(user);
                }
                Console.WriteLine("{0} added", user.DistinguishedName);
            }
        }


        private static void PopulateDirectoryForTests()
        {
            var user2Container = DnParser.ParseName(TestUserDirectoryContainer);
            var user2Prefix = DnParser.ParseRDN(TestUserDirectoryContainer);
            AddContainerIfNecessary(user2Prefix, user2Container);

            using (var context = new DirectoryContext())
            {
                AddEntryIfNecessary("CN=PasswordUser," + TestUserDirectoryContainer, "user", context);
                AddEntryIfNecessary("CN=persontest," + TestUserDirectoryContainer, "user", context);
                AddEntryIfNecessary("CN=TestUser," + TestUserDirectoryContainer, "user", context);
                AddEntryIfNecessary("CN=TestUser2," + TestUserDirectoryContainer, "user", context);
            }

            var roleContainer = DnParser.ParseName(RolesDirectoryContainer);
            var rolePrefix = DnParser.ParseRDN(RolesDirectoryContainer);
            AddContainerIfNecessary(rolePrefix, roleContainer);

            using (var context = new DirectoryContext())
            {
                IDirectoryAttributes rangeTest = AddEntryIfNecessary("CN=RangeTest," + RolesDirectoryContainer, "group", context);

                foreach (var kvp in rangeTest.Where(kvp => kvp.Key.StartsWith("member", StringComparison.OrdinalIgnoreCase)))
                {
                    if (kvp.Value is IEnumerable<string> && ((IEnumerable<string>)kvp.Value).Any())
                    {
                        Console.WriteLine("RangeTest members already populated");
                        return;
                    }
                }

                var newMembers = new List<string>();
                newMembers.AddRange(context.Query<LdsUser>().Take(10000).Select(u => u.DistinguishedName));
                rangeTest.Set("member", newMembers);

                context.Update(rangeTest);
            }
        }

        #endregion PopluateDirectoryForTests Code

        private static void EnablePasswordChangesOn389()
        {
            using (var context = new DirectoryContext())
            {
                var attributes = context.ListServerAttributes("namingcontexts");

                var namingContexts = attributes.GetStrings("namingcontexts");

                var configurationDN = namingContexts != null
                                          ? namingContexts
                                                .FirstOrDefault(s => DnParser.ParseName(s)
                                                    .Equals("Configuration", StringComparison.OrdinalIgnoreCase))
                                          : null;

                bool success = false;
                if (configurationDN != null)
                {
                    var directoryService = context.Query(configurationDN)
                        .Where(_ => Filter.Equal(_, "cn", "Directory Service", true))
                        .Select("distinguishedName")
                        .FirstOrDefault();

                    if (directoryService != null)
                    {
                        directoryService.Set("dSHeuristics", "0000000001001");

                        context.Update(directoryService);
                        success = true;
                    }
                }
                
                if (!success)
                {
                    Console.WriteLine("Could not set a password policy");
                }
            }
        }

        private static IDirectoryAttributes AddContainerIfNecessary(Type type)
        {
            var mapping = LdapConfiguration.Configuration.Mapper.GetMapping(type);

            var containerCn = DnParser.ParseName(mapping.NamingContext);
            var containerPrefix = DnParser.ParseRDN(mapping.NamingContext);

            return AddContainerIfNecessary(containerPrefix, containerCn);
        }

        private static IDirectoryAttributes AddContainerIfNecessary(string prefix, string containerName)
        {
            using (var context = new DirectoryContext())
            {
                var dn = prefix + "=" + containerName + "," + ServerRoot;

                return AddEntryIfNecessary(dn, "container", context);
            }
        }

        private static IDirectoryAttributes AddEntryIfNecessary(string dn, string objectClass, IDirectoryContext context)
        {
            IDirectoryAttributes entry = null;
            try
            {
                entry = context.GetByDN(dn);
            }
            catch (DirectoryOperationException ex)
            {
                if (ex.Message.IndexOf("object does not exist", StringComparison.OrdinalIgnoreCase) == -1)
                {
                    throw;
                }
            }

            if (entry == null)
            {
                entry = new DirectoryAttributes(dn);
                entry.Set("objectClass", objectClass);

                Console.WriteLine("Adding {0}", dn);
                return context.AddAndGet(entry);
            }

            Console.WriteLine("{0} already exists", dn);
            return entry;
            
        }
    }
}
